/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE */
package au.com.lastweekend.ws2300.proxy;

import java.io.IOException;
import java.util.Calendar;
import java.util.TimeZone;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import au.com.lastweekend.ws2300.Device;
import au.com.lastweekend.ws2300.MemoryMap;

/**
 * This class tries to behave like the real thing
 * 
 * Ultimately could simulate sensor values.
 * 
 * @author ggardner
 * 
 */
public class WS2300Simulator {

    private static final Logger LOG = LoggerFactory.getLogger(WS2300Simulator.class);

    private boolean stopped = false;
    private int timeWarp = 1000;

    private MemoryDevice device;

    /**
     * Note this clock is deliberately inaccurate like the real device!!
     * 
     * @author ggardner
     * 
     */
    public class ClockThread extends Thread {

        private Calendar cal;

        public ClockThread() {

            super("WS2300Simulator-Clock");
            // setDaemon(true);
            cal = Calendar.getInstance();
            cal.setTimeZone(TimeZone.getDefault());
            cal.setFirstDayOfWeek(Calendar.MONDAY);
            try {
                initDateLastSet();
                setUTCTime();
                setLocalTime();
            } catch (IOException e) {
                // Not Expected with MemoryDevice;
            }
        }

        @Override
        public void run() {

            long prevHash = 0;
            while (!stopped) {
                try {
                    byte[] dlsNibbles = device.read(MemoryMap.DATE_LAST_SET, 3);
                    byte[] utcNibbles = device.read(MemoryMap.TIME_UTC, 3);

                    int day = Device.bcd(dlsNibbles, 0, 2);
                    int month = Device.bcd(dlsNibbles, 2, 2) - 1;
                    int year = Device.bcd(dlsNibbles, 4, 2) + 2000;
                    int seconds = Device.bcd(utcNibbles, 0, 2);
                    int minutes = Device.bcd(utcNibbles, 2, 2);
                    int hours = Device.bcd(utcNibbles, 4, 2);

                    long setHash = hashTime(year, month, day, hours, minutes, seconds);

                    if (setHash != prevHash) {
                        // TODO TimeZone
                        cal.set(year, month, day, hours, minutes, seconds);
                        LOG.debug(String.format("Time set detected %tc", cal));

                    } else {
                        cal.add(Calendar.SECOND, 1);
                    }

                    LOG.debug(String.format("Time: %tc", cal));
                    setUTCTime();

                    if (seconds == 59) {

                        setLocalTime();
                    }

                    prevHash = hashTime(year, month, day, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), cal
                            .get(Calendar.SECOND));

                    sleep(timeWarp);

                } catch (IOException e1) {
                    // Not expected for Memory Device.

                } catch (InterruptedException e) {
                    // oh well;
                }
            }

        }

        private long hashTime(int year, int month, int day, int hours, int minutes, int seconds) {

            return seconds * minutes * 60 + hours * 3600 + day * (3600 * 24) + month * (3600 * 24 * 31) + (year - 2000)
                    * (3600 * 24 * 31 * 12);
        }

        private void initDateLastSet() throws IOException {

            byte[] nibbles = new byte[6];
            Device.bcdEncode(cal.get(Calendar.DAY_OF_MONTH), nibbles, 0, 2);
            Device.bcdEncode(cal.get(Calendar.MONTH) + 1, nibbles, 2, 2);
            Device.bcdEncode(cal.get(Calendar.YEAR) - 2000, nibbles, 4, 2);

            device.write(MemoryMap.DATE_LAST_SET, nibbles);

        }

        private void setUTCTime() throws IOException {

            byte[] nibbles = new byte[6];
            Device.bcdEncode(cal.get(Calendar.SECOND), nibbles, 0, 2);
            Device.bcdEncode(cal.get(Calendar.MINUTE), nibbles, 2, 2);
            Device.bcdEncode(cal.get(Calendar.HOUR_OF_DAY), nibbles, 4, 2);

            device.write(MemoryMap.TIME_UTC, nibbles);
        }

        private void setLocalTime() throws IOException {

            byte[] nibbles = new byte[11];
            Device.bcdEncode(cal.get(Calendar.MINUTE), nibbles, 0, 2);
            Device.bcdEncode(cal.get(Calendar.HOUR_OF_DAY), nibbles, 2, 2);
            Device.bcdEncode((((cal.get(Calendar.DAY_OF_WEEK) + 6) % 7) + 1), nibbles, 4, 1);
            Device.bcdEncode(cal.get(Calendar.DAY_OF_MONTH), nibbles, 5, 2);
            Device.bcdEncode(cal.get(Calendar.MONTH) + 1, nibbles, 7, 2);
            Device.bcdEncode(cal.get(Calendar.YEAR) - 2000, nibbles, 9, 2);

            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("Setting local time to %tc", cal));
            }

            device.write(MemoryMap.TIME_LOCAL, nibbles);
        }

    }

    public WS2300Simulator(MemoryDevice device) {

        this.device = device;
    }

    /**
     * The number of real milliseconds to use to drive the clock
     * 
     * @param timeWarp
     */
    public void setTimeWarp(int timeWarp) {

        this.timeWarp = timeWarp;
    }

    public void stop() {

        stopped = true;
    }

    private void startClockThread() {

        new ClockThread().start();

    }

    public void start() {

        startClockThread();

    }

}
